package dila

import (
	"context"
	"fmt"
	"go.uber.org/zap"
	"strconv"
	"sync"
	"sync/atomic"
	"time"
	"wzgames/internal/client"
	"wzgames/internal/gloader"
	"wzgames/internal/model/gamep"
	"wzgames/internal/model/mcomm"
	"wzgames/internal/model/statement"
	"wzgames/pkg/actor/actor"
	"wzgames/pkg/actor/actorservice"
	"wzgames/pkg/common/timingwheel"
	"wzgames/pkg/logger"
	"wzgames/pkg/utils/dateutil"
)

// 游戏结算处理

const (
	defaultTimeout3  = time.Second * 3
	defaultTimeout15 = time.Second * 15
)

var (
	dealer *Dealer
	once   sync.Once
)

type Dealer struct {
	pid      *actor.PID
	mu       sync.RWMutex
	start    int64
	ctx      context.Context
	cancel   context.CancelFunc
	startStm *statement.NodeStartStmSettle
	limit1   int64

	settleChan chan *statement.StmState // 游戏结算
	avLoseChan chan *statement.StmState // 小飞机输的结算
	waitChan   chan struct{}
	settleQue  *Queue
	workChan   chan struct{}
	workN      int64

	timerWheel *timingwheel.TimingWheel
	stop       bool
}

func Init(id int64, tm *timingwheel.TimingWheel) {
	if dealer == nil {
		once.Do(func() {
			dealer = newDealer(id, tm)
		})
	}
}

func newDealer(id int64, tm *timingwheel.TimingWheel) *Dealer {
	d := &Dealer{
		timerWheel: tm,
		limit1:     30,
		start:      dateutil.UnixMilli(),
		settleChan: make(chan *statement.StmState, 20),
		avLoseChan: make(chan *statement.StmState, 20),
		waitChan:   make(chan struct{}, 1),
		workChan:   make(chan struct{}, 1),
	}

	name := fmt.Sprintf("dila-%s", strconv.FormatInt(id, 10))
	props := actor.PropsFromProducer(func() actor.Actor { return d })
	pid, err := actorservice.ActorSystem().Root.SpawnNamed(props, name)
	if err != nil {
		logger.Error("[DiLa] spawn dealer actor error", zap.Int64("Id", id), zap.Error(err))
		d = nil
		return nil
	}
	client.NodeBootRecord(id, d.start)
	d.startStm = &statement.NodeStartStmSettle{
		NodeId:    id,
		Start:     d.start,
		LastBetId: 0,
		LimitNum:  d.limit1,
		Dateline:  d.start,
	}
	d.pid = pid
	d.ctx, d.cancel = context.WithCancel(context.Background())
	d.settleQue = NewQue()
	go d.dispatch()
	d.nextWaitTick()
	go d.worker()
	d.nextWorkTick()
	d.nextStartTick(defaultTimeout3)

	return d
}

func (d *Dealer) Pid() *actor.PID {
	return d.pid
}

func (d *Dealer) Receive(ctx actor.Context) {
	switch m := ctx.Message().(type) {
	case statement.PlayState: // 待结算分流
		d.processState(m)
	case *statement.SettleStmList: // 批量处理待结算列表
		d.processSettleList(m)
	case *statement.NodeStartStmSettle:
		d.nodeStartSettleStmDeal()
	default:
	}
}

func (d *Dealer) processState(ste statement.PlayState) {
	defer logger.Recover()
	if d.stop {
		return
	}
	item := &statement.StmState{
		BetId:      ste.BetId,
		GAct:       ste.GAct,
		GameId:     ste.GameId,
		RoundId:    ste.RoundId,
		Multiplier: ste.Multiplier,
	}
	switch ste.GAct {
	case mcomm.GameAction_GameImResult, mcomm.GameAction_GameCashOut:
		d.settleChan <- item
	case mcomm.GameAction_GameLoseSettle:
		if ste.GameId == gamep.AviatorGameId {
			d.avLoseChan <- item
		} else {
			d.settleChan <- item
		}
		//case mcomm.GameAction_GameCancel:
		//case mcomm.GameAction_GameBet:
		//case mcomm.GameAction_GameUpStep:
	}
}

func (d *Dealer) nextWaitTick() {
	d.timerWheel.AfterFunc(defaultTimeout3, d.sendWaitTick, nil)
}

func (d *Dealer) sendWaitTick(args interface{}) {
	if d.stop {
		return
	}
	d.waitChan <- struct{}{}
}

// dispatch 待结算入队列
func (d *Dealer) dispatch() {
	ss := d.genSettleStmList(0) // other
	c1 := 0
	av := d.genSettleStmList(1) // aviator lose settle
	c2 := 0
	for {
		select {
		case state := <-d.settleChan:
			if c1 >= 20 {
				ss1 := ss
				d.settleQue.Push(ss1)
				ss = d.genSettleStmList(0)
				c1 = 0
			}
			c1++
			ss.States = append(ss.States, state)
		case avState := <-d.avLoseChan:
			if c2 >= 20 {
				av1 := av
				d.settleQue.Push(av1)
				av = d.genSettleStmList(1)
				c2 = 0
			}
			c2++
			av.States = append(av.States, avState)
		case <-d.waitChan:
			if c1 > 0 {
				ss1 := ss
				d.settleQue.Push(ss1)
				ss = d.genSettleStmList(0)
				c1 = 0
			}
			if c2 > 0 {
				av1 := av
				d.settleQue.Push(av1)
				av = d.genSettleStmList(1)
				c2 = 0
			}
			d.nextWaitTick()
		case <-d.ctx.Done():
			return
		}
	}
}

// worker 处理结算队列
func (d *Dealer) worker() {
	for {
		select {
		case <-d.workChan:
			val := atomic.LoadInt64(&d.workN)
			if val >= 10 {
				d.nextWorkTick()
			} else {
				req := d.settleQue.Pop()
				if req == nil {
					d.nextWorkTick()
				} else {
					atomic.AddInt64(&d.workN, 1)
					actorservice.Send(d.pid, req)
					v, ok := req.(*statement.SettleStmList)
					if ok {
						if v.FailCount == 0 {
							d.sendWorkTick(nil)
						} else {
							d.nextWorkTick()
						}
					} else {
						d.nextWorkTick()
					}
				}
			}
		case <-d.ctx.Done():
			return
		}
	}
}

func (d *Dealer) nextWorkTick() {
	d.timerWheel.AfterFunc(time.Second, d.sendWorkTick, nil)
}

func (d *Dealer) sendWorkTick(args interface{}) {
	if d.stop {
		return
	}
	d.workChan <- struct{}{}
}

func (d *Dealer) processSettleList(req *statement.SettleStmList) {
	defer logger.Recover()
	defer func() {
		atomic.AddInt64(&d.workN, -1)
	}()
	reply := &mcomm.CodeRet{}
	rpcCtx, cancelFn := context.WithTimeout(context.Background(), defaultTimeout15)
	err := client.GetUCClient().Call(rpcCtx, "UpBetPendingSettle", req, reply)
	cancelFn()
	ok := true
	if err != nil {
		logger.Error("[DiLa] update bet pending settle fail", zap.Int64("NodeId", req.NodeId), zap.Error(err))
		ok = false
	}
	if reply.Code != 0 || !ok {
		req.FailCount += 1
		if req.FailCount <= 10 { // 失败重试
			d.settleQue.Push(req)
		}
	}
	return
}

func (d *Dealer) genSettleStmList(gType int32) *statement.SettleStmList {
	return &statement.SettleStmList{
		NodeId:   gloader.BootConfig().Node().NodeId,
		GType:    gType,
		Dateline: dateutil.UnixMilli(),
		Op:       0,
		States:   []*statement.StmState{},
	}
}

// nodeStartSettleStmDeal 游戏节点启动后执行待结算的流水生成
func (d *Dealer) nodeStartSettleStmDeal() {
	defer logger.Recover()
	reply := &statement.NodeStartStmSettleReply{}
	rpcCtx, cancelFn := context.WithTimeout(context.Background(), defaultTimeout15)
	err := client.GetUCClient().Call(rpcCtx, "NodeStartSettledStmDeal", d.startStm, reply)
	cancelFn()
	ok := true
	if err != nil {
		logger.Error("[DiLa] node start bet pending settle stm fail", zap.Int64("NodeId", d.startStm.NodeId), zap.Error(err))
		ok = false
	}
	if reply.Code != 0 || !ok {
		d.nextStartTick(defaultTimeout3)
		return
	}
	d.startStm.LastBetId = reply.LastBetId
	if reply.DealCount < d.limit1 { // finish
		//fmt.Println("finish")
	} else {
		d.nextStartTick(time.Second)
	}
}

func (d *Dealer) nextStartTick(t time.Duration) {
	d.timerWheel.AfterFunc(t, d.doNodeStart, nil)
}

func (d *Dealer) doNodeStart(args interface{}) {
	if d.stop {
		return
	}
	actorservice.Send(d.pid, d.startStm)
}

func (d *Dealer) Stop() {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.stop = true
	close(d.settleChan)
	close(d.avLoseChan)
	close(d.waitChan)
	close(d.workChan)
	d.cancel()
	actorservice.Stop(d.pid)
}

func SendPlayState(betId uint64, gAct mcomm.GameAction, gameId int32, roundId int64) {
	SendPlayStateWithMultiplier(betId, gAct, gameId, roundId, 0)
}

func SendPlayStateWithMultiplier(betId uint64, gAct mcomm.GameAction, gameId int32, roundId int64, multiplier float64) {
	if dealer != nil {
		actorservice.Send(dealer.Pid(), statement.PlayState{BetId: betId, GAct: gAct, GameId: gameId, RoundId: roundId, Multiplier: multiplier})
	}
}

func Stop() {
	if dealer != nil {
		dealer.Stop()
	}
}
